Guard是一個帶有@Guard()裝飾器的類,Guard要去實作CanActivate介面。
Guard只做一件事情,就是擔任路由警衛,決定程式在收到HTTP請求後,是否要執行 route handler。 先前有介紹到Middleware,為何不使用Middleware作為路由警衛?因為Middleware只要有呼叫next(),必然會讓程式繼續執行下一個事件,所以不適合,角色權限的設計也會更顯麻煩。
注意:客戶發動請求的流程為 Request->Middleware->Guard->Pipe->route handler。
cd src/modules/Shared & mkdir Guards
程式碼如下:
src/modules/Shared/Guards/roles.guard.ts
import { Guard, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs/Observable';
@Guard()
export class RolesGuard implements CanActivate {
/*
1.dataOrRequest參數代表你可以傳入expressjs中的request object、或經由microservice、websocket傳遞的data。
2.ExecutionContext帶有兩個成員,parent和handler,其中,parent代表哪個Controller,handler是route handler的參考。
3.Promise<boolean> | Observable<boolean>,代表路由警衛可以用async寫法。
*/
canActivate(dataOrRequest, context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
/*true會通過,false會回傳
{
"statusCode": 403,
"message": "Forbidden resource"
}
*/
return true;
}
}
說明:我們先一睹Guard樣貌,後續會有較詳細的實作。
Guard跟Middleware和Pipe一樣可以作用在Method、Controller、Global。
import{ UseGuards } from '@nestjs/common';
import { RolesGuard } from '../Shared/Guards/roles.guard';
@Controller()
@UseGuards(RolesGuard)
//@UseFilters(new HttpExceptionFilter())
export class UsersController {...省略...}
說明:正常顯示。
@Guard()
export class RolesGuard implements CanActivate {
/*
1.dataOrRequest參數代表你可以傳入expressjs中的request object、或經由microservice、websocket的data。
2.ExecutionContext帶有兩個成員,parent和handler,parent代表哪個Controller,handler是route handler的參考。
3.Promise<boolean> | Observable<boolean>,代表路由警衛可以用async寫法。
*/
canActivate(dataOrRequest, context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
/*true會通過,false會回傳
{
"statusCode": 403,
"message": "Forbidden resource"
}
*/
return false;
}
}
說明:符合false的預期。
cd src/modules/Shared & mkdir Decorators
6.2 在Decorators資料夾新增roles.decorator.ts。
src/modules/Shared/Decorators/roles.decorator.ts
程式碼如下:
import { ReflectMetadata } from '@nestjs/common';
//實作一個@Roles()裝飾器
export const Roles = (...roles: string[]) => ReflectMetadata('roles', roles);
6.3 修改RolesGuard。
程式碼如下:
src/modules/Shared/Guards/roles.guard.ts
import { Guard, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs/Observable';
import { Reflector } from '@nestjs/core';
@Guard()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) { }
canActivate(req, context: ExecutionContext): boolean {
const { parent, handler } = context;
const roles = this.reflector.get<string[]>('roles', handler);
if (!roles) {
return true;
}
/*req.user是假資料,這是在模擬登入後,有一組user資訊放在req object裡,
也可以放在session等,登入資訊的roles表示角色權限,是陣列,一個帳號可能有多個角色。
而Ted的角色是general,能夠請求通過帶有@Roles('general')裝飾器的目標。
*/
req.user = { "account": "Ted", "roles": ["general"] };
const user = req.user;
const hasRole = () => !!user.roles.find((role) => !!roles.find((item) => item === role));
return user && user.roles && hasRole();
}
}
說明:請看註解。
6.4 UsersController做些小變化。
程式碼如下:
src/modules/Users
@Controller()
@UseGuards(RolesGuard)
//@UseFilters(new HttpExceptionFilter())
export class UsersController {...省略...}
@Get('users')
@Roles('admin')
//使用Express的參數
async getAllUsers( @Request() req, @Response() res, @Next() next) {...省略...}
@Get('users/:id')
@Roles('general')
//使用Express的參數
//@Param('id')可以直接抓id參數
//使用ParseIntPipe
async getUser( @Response() res, @Param('id', new ParseIntPipe()) id) {...省略...}
@UseGuards(RolesGuard)代表整個UsersController的方法都會去執行路由警衛機制,@Roles('xxx')代表只有該角色才能訪問,預期是http://localhost:3000/users 不能被訪問,但http://localhost:3000/users/1 可以被訪問。
6.5 打開Postman 對http://localhost:3000/users 發出GET請求,結果如下:
符合預期,因為getAllUsers方法帶有@Roles('admin')裝飾器,但Ted的role是general,所以被拒絕訪問。
6.6 打開Postman 對http://localhost:3000/users/1 發出GET請求,結果如下:
符合預期,因為getUser方法帶有@Roles('general')裝飾器,Ted的role是general,所以允許訪問。
以上大家已經知道怎麼實作Guard、Role,這可以讓我們更容易管理我們的route handler的訪問權限。
程式在github